feature/light propagation#916
Conversation
…o preserve injections from dynamic light mods (e.g. SodiumDynamicLights). Add backward compatibility shims for EntitySubLevelUtil.getEyePositionInterpolated and getTrackingSubLevel to prevent NoSuchMethodError with Simulated/Create Aeronautics.
…s and other sub-levels. Uses a virtual light spread system with manhattan distance falloff on the client side, injected into both vanilla and Sodium light pipelines.
…minate nearby sub-level blocks. Scans for light emitters near each sub-level's visual position on the server, transforms their positions into plot-local coordinates, and injects them into the plot's light engine for proper propagation. Rescans on light changes, sub-level creation/split, and every 8 blocks of movement.
…s and other sub-levels. Uses a virtual light spread system with manhattan distance falloff on the client side, injected into both vanilla and Sodium light pipelines.
Light-emitting blocks on one sub-level now illuminate nearby other sub-levels. Emitters are discovered by scanning other sub-levels' plot chunks, transforming positions to world space, and injecting them into the receiving sub-level's plot light engine. Detection: LevelChunkMixin tracks when a block's light emission changes on a sub-level plot and notifies nearby sub-levels to rescan. Only triggers when emission actually changes (oldState vs newState), avoiding unnecessary work from non-light block updates. Movement: When any sub-level moves, nearby sub-levels are marked for rescan so they pick up emitters at updated world-space positions. Cleanup fix: Old injected positions are cleared unconditionally during reinject, since they are plot-local coordinates that cannot be validated against world state. Performance: Light updates now use ClientboundLightUpdatePacket instead of full chunk resends (ClientboundLevelChunkWithLightPacket), avoiding client-side chunk state resets that caused visual glitches with other mods (e.g. Create ropes) and reducing network overhead.
… and light updates
|
|
cakeGit
left a comment
There was a problem hiding this comment.
Im not an authority here but im concerned about the quality of this code. From my experience, the fully qualified names, duplicated code and overly complex structure indicates this is written with some coding agent without enough oversight, just like the last PR. This is a very large system and should be much more carefully implemented, by someone who understands the system better. This should be closed the same as #415
| * <li>Neighbor notification: when a sub-level changes, nearby sub-levels re-propagate</li> | ||
| * </ul> | ||
| */ | ||
| public final class ServerSubLevelWorldInjector { |
There was a problem hiding this comment.
The class is named quite vaguely and doesen't mention light, i would call it something like "SubLevelToWorldLighter" and the other class be called "WorldToSubLevelLighter"
|
|
||
| private static final double LIGHT_MARGIN = 15.0; | ||
|
|
||
| private static final ConcurrentHashMap<UUID, LongSet> injectedPositions = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
This many ConcurrentHashMaps seems unnecassary, could you not hold a single ConcurrentHashMap<UUID, SubLevelLightInfo> or something
| /** | ||
| * Called once per server tick after all plots have ticked, from ServerSubLevelContainer.tick(). | ||
| */ | ||
| public static void tick(final ServerLevel level) { |
There was a problem hiding this comment.
Incredibly long tick needs to be broken down,
I call it comment blocking but each of the phases seem they could probably be much better as separate methods
| */ | ||
| public final class ServerSubLevelLightInjector { | ||
|
|
||
| private static final double LIGHT_MARGIN = 15.0; |
| // if it's only air, all zeros will do. it'll default to empty neighborhood state and 0 (empty) collider ID | ||
| if (!section.hasOnlyAir()) { | ||
| final LevelChunk chunk = this.accelerator.getChunk(x, z); | ||
| if (chunk == null) return; |
There was a problem hiding this comment.
Whats the reason for this? As far as i can see accelerator.getChunk is nonnull
| * Cached plot-local block data for a sub-level's light-relevant blocks. | ||
| * Populated once on block change, re-projected to world space on movement. | ||
| */ | ||
| public final class PlotLocalLightData { |
| * Called when a light-emitting block changes on a sub-level's plot. | ||
| */ | ||
| public static void onPlotBlockLightChanged(final ServerLevel level, final ServerSubLevel changedSubLevel) { | ||
| final var container = dev.ryanhcode.sable.api.sublevel.SubLevelContainer.getContainer(level); |
There was a problem hiding this comment.
Theres consistent use of fully qualified names instead of imports
Intro
Another attempt of mine to add light propagation to\from sublevels, now fully done on server side, so injected light also has gameplay effects instead of being client side visual
There are a lot of architectural changes since the last PR, also did a pass over that version to comply with SLA
Current version fully working and can be tested if you wish to check performance, however I fully understand that such changes shouldn't be just merged into main. My goal is to propose the architecture and share my draft for your consideration. Edge cases i haven't fixed will be listed in the end
I have been hosting a server with this updated version for a week and so far hasn't recieved any complaints from players and didn't notice much issues myself. Performance difference between main Sable and this fork is almost negligible, if Spark is to be trusted
Architecture
There are two light injectors (world->plot and reverse) and one opacity mixin
ServerSubLevelLightInjector, world->plotRuns from ServerLevelPlot.tick() per sub-level. Injects world emitters and emitters from other sublevels (that are injected to world).
tickPlotdetects movement and does either:fullRescan-- mostly used on the first tick to get initial state, scans full light box (16 blocks in each direction)incrementalScan-- shift the cached emitter set and scan only the leading slice(s), done on translationboundsDiffScan-- scan only regions added by rotationscanOtherSubLevelEmittersfinds emitters on overlapping sub-levels and projects them inscanWorldOpaqueIntoPlotprojects world and other sub-levels opaque blocks into plot space, stored in plotLocalWorldOpaquereinjectclears old positions in the plot's LevelLightEngine, writes the new ones via setStoredLevel + enqueueIncrease, and runs propagationServerSubLevelWorldInjector, plot → worldRuns from ServerSubLevelContainer.tick() once per server tick, after every sub-level's tickPlot has run. Has five phases:
detectMovement-- edge crossing on bounding box corners flags the sub-level for updaterebuildPlotLocalCache(only on block change) reads the plot once and stores aPlotLocalLightDatarecord: arrays of plot positions for emitters, opaque blocks and shape-occluding blocks (slabs/stairs).projectToWorldSpacethen transforms those cached positions into world space using the current poseopaquePositionsCore(no gap-fills) andshapeOcclusionMapare rebuilt from per sub-level slices, world emitters near changed sub-levels are then re-propagated on the light thread (clear + re-emit), so they respect the new opacity geometryLevelLightEngineviataskMailboxCaches
Main optimization since last version i submitted -- sub-levels now cache a lot of stuff to reduce full scans
PlotLocalLightDataper sub-levelcachedPlotSourcesper sub-levelperSubLevelOpaque,perSubLevelOpaqueCore,perSubLevelShapesopaquePositions/opaquePositionsCore/shapeOcclusionMapcachedWorldSources(per sub-level)plotLocalWorldOpaque(per sub-level)injectedPositions(per sub-level)Known bugs